package com.surrealbananapeel.battleship;

import java.util.ArrayList;
import java.util.Random;

/**
 * This is the shooter class. It is responsible for shooting. It works 
 * by getting stats from the match and then making a shot, or just shooting 
 * randomly.
 * 
 * @author Emmett Little and Alberto Saavedra
 */
public class Shooter
{
    private static Random rand = new Random();
    private ArrayList<Point> shots;
    private ArrayList<Point> starterShots;
    private Match match;
    private int offset;
    
    /**
     * Create a new instance of the shooter class.
     */
    public Shooter()
    {
        this.match = Match.getInstance();
        this.shots = new ArrayList<Point>();
        this.starterShots = new ArrayList<Point>();
        this.offset = rand.nextInt(BattleshipClient.WIDTH);
        populateStarterShots();
        populateShots();
    }
    
    /**
     * This method adds some standard starter shots on the edges, middles, 
     * and sides. It will also add common ship placements from previous 
     * games.
     */
    private void populateStarterShots()
    {
        final int x = BattleshipClient.WIDTH - 1;
        final int y = BattleshipClient.HEIGHT - 1;
        final int z = BattleshipClient.DEPTH - 1;
        final int dx = x / 4;
        final int dy = y / 4;
        final int dz = z / 4;
        
        // middles
        starterShots.add(new Point(dx, dy, dz));
        starterShots.add(new Point(x - dx, dy, dz));
        starterShots.add(new Point(x - dx, dy, z - dz));
        starterShots.add(new Point(dx, dy, z - dz));
        starterShots.add(new Point(dx, y - dy, dz));
        starterShots.add(new Point(x - dx, y - dy, dz));
        starterShots.add(new Point(x - dx, y - dy, z - dz));
        starterShots.add(new Point(dx, y - dy, z - dz));
        
        // corners
        starterShots.add(new Point(0, 0, 0));
        starterShots.add(new Point(x, 0, 0));
        starterShots.add(new Point(0, y, 0));
        starterShots.add(new Point(0, 0, z));
        starterShots.add(new Point(x, y, 0));
        starterShots.add(new Point(x, 0, z));
        starterShots.add(new Point(0, y, z));
        starterShots.add(new Point(x, y, z));
        
        // sides
        starterShots.add(new Point(x / 2, 0, 0));
        starterShots.add(new Point(x, 0, z / 2));
        starterShots.add(new Point(x / 2, 0, z));
        starterShots.add(new Point(0, 0, z / 2));
        starterShots.add(new Point(0, y / 2, 0));
        starterShots.add(new Point(x, y / 2, 0));
        starterShots.add(new Point(x, y / 2, z));
        starterShots.add(new Point(0, y / 2, z));
        starterShots.add(new Point(x / 2, y, 0));
        starterShots.add(new Point(x, y, z / 2));
        starterShots.add(new Point(x / 2, y, z));
        starterShots.add(new Point(0, y, z / 2));
        
        addCommonShots();
    }
    
    /**
     * This will get the most common shots and add them to the starter shot 
     * list. The starter shot list will not grow about 40.
     */
    private void addCommonShots()
    {
        ArrayList<Point> hits = (ArrayList<Point>) this.match.getSBPStats();
        for (int i = hits.size()-1; i >= 0; i--)
        {
            starterShots.add(0, hits.get(i));
            if (starterShots.size() > 35)
            {
                starterShots.remove(starterShots.size()-1);
            }
        }
    }
    
    /**
     * When the shots list runs dry this method is called. It will populate 
     * the list by calling populateShotsOffset with an offset of 0. If no 
     * shots are returned then it is called again with an offset of 2.
     */
    private void populateShots()
    {
    	populateShotsOffset(0);
    	if (shots.size() == 0)
    	{
    		populateShotsOffset(2);
    	}
    }
    
    /**
     * When the shots list runs dry this method is called. It will populate 
     * the list by creating diagonals shots shifted along the z axis.
     * 
     * @param xs The offset
     */
    private void populateShotsOffset(int xs)
    {
    	for (int i = 0; i < BattleshipClient.DEPTH; i++)
        {
            for (int j = 0; j < BattleshipClient.HEIGHT; j++)
            {
                Point p1 = new Point((offset + i + j + xs) % BattleshipClient.WIDTH, j, i);
                if (!this.match.hasBeenShot(p1))
                {
                    shots.add(p1);
                }
                
                Point p2 = new Point((offset + 5 + j + i + xs) % BattleshipClient.WIDTH, j, i);
                if (!this.match.hasBeenShot(p2))
                {
                    shots.add(p2);
                }
            }
        }
    }
    
    /**
     * This method determines what shot is to be shot. First shoot starter 
     * shots, then shoot pattern, but always shoot at hits and if all else 
     * fails then shoot a random shot.
     * 
     * @return The point to shoot at
     */
    public Point shoot()
    {        
        ArrayList<Point> hits = this.match.currentGame().getHits();
        ArrayList<Point> detected = this.match.currentGame().getDetecteds();
        
        if ((detected.size() + hits.size() >= 32)
                && (detected.size() != this.match.currentGame()
                        .getRealDetecteds() || hits.size() != this.match
                        .currentGame().getRealHits()))
        {
            // detection f***ed up, get a bunch of shots
            shots = new ArrayList<Point>();
            for (int i = 0; i < hits.size(); i++)
            {
                addSurroundingPoints(hits.get(i));
            }
            if (shots.size() == 0)
            {
	            for (int i = 0; i < detected.size(); i++)
	            {
	                addSurroundingPoints(detected.get(i));
	            }
            }
            if (shots.size() == 0)
            {
	            for (int i = 0; i < hits.size(); i++)
	            {
	                addEnclosingPoints(hits.get(i));
	            }
            }
            if (shots.size() == 0)
            {
	            for (int i = 0; i < detected.size(); i++)
	            {
	                addEnclosingPoints(detected.get(i));
	            }
            }
            
        }
        else if (hits.size() == 0 && starterShots.size() == 0)
        {
            // get more shots
            populateShots();
        }
        else if (hits.size() > 0) 
        {
            // shoot around the hits
            shots = new ArrayList<Point>();
            for (int i = 0; i < hits.size(); i++)
            {
                addSurroundingPoints(hits.get(i));
            }
        }
        else if (starterShots.size() != 0)
        {
        	Point p;
        	do
        	{
        		p = starterShots.remove(rand.nextInt(starterShots.size()));
        	} while (this.match.hasBeenShot(p) && starterShots.size() > 0);
            return p;
        }
        
        // shoot a random shot
        if (this.shots.size() != 0)
        {
            return this.shots.remove(rand.nextInt(this.shots.size()));
        }
        else
        {
            populateShots();
            if (this.shots.size() != 0)
            {
                return this.shots.remove(rand.nextInt(this.shots.size()));
            }
            else
            {
                return randomShot();
            }
        }
    }
    
    /**
     * This will return a random shot that has not yet been been fired.
     * 
     * @return A random shot
     */
    private Point randomShot()
    {
        Point point;
        do
        {
            point = new Point(rand.nextInt(BattleshipClient.WIDTH), 
                    rand.nextInt(BattleshipClient.HEIGHT), 
                    rand.nextInt(BattleshipClient.DEPTH));
        } while (match.hasBeenShot(point));
        
        return point;
    }
    
    /**
     * This will add up to 26 points completely surrounding the supplied 
     * point to the list. This is done only if detection messed up.
     * 
     * @param p The point to enclose
     */
    private void addEnclosingPoints(Point p)
    {
        for (int x = -1; x <= 1; x++)
        {
            for (int y = -1; y <= 1; y++)
            {
                for (int z = -1; z <= 1; z++)
                {
                    addNewPoint(p, x, y, z);
                }
            }
        }
    }
    
    /**
     * This will add up to 6 points on each face of the supplied point to 
     * the list.
     * 
     * @param p The point to surround
     */
    private void addSurroundingPoints(Point p)
    {
        addNewPoint(p, 1, 0, 0);
        addNewPoint(p, -1, 0, 0);
        addNewPoint(p, 0, 1, 0);
        addNewPoint(p, 0, -1, 0);
        addNewPoint(p, 0, 0, 1);
        addNewPoint(p, 0, 0, -1);
    }
    
    /**
     * Adds a new point to the shot list based off the supplied list and 
     * the offsets. The point is only added if it has yet to be shot.
     * 
     * @param point The point to copy
     * @param x The x offset of the new point
     * @param y The y offset of the new point
     * @param z The z offset of the new point
     */
    private void addNewPoint(Point point, int x, int y, int z)
    {
        Point p = new Point(point.toString());
        p.addX(x);
        p.addY(y);
        p.addZ(z);
        if (!this.match.hasBeenShot(p))
        {
            shots.add(p);
        }
    }
}
